/***************************************************************************
 *   Antc - library                                                        *
 *                                                                         *
 *   Copyright (C) 2008 by kuerant                                         *
 *                         <kuerant@gmail.com>                             *
 *                                                                         *
 *   This library is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU Library General Public License as       *
 *   published by the Free Software Foundation; either version 2 of the    *
 *   License, or (at your option) any later version.                       *
 *                                                                         *
 *   This library is distributed in the hope that it will be usefull, but  *
 *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      *
 *   Library General Public License for more details.                      *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with this library; if not, write to the Free Software   *
 *   Foundation, Inc., 59 Temple Place - Suite 330, Boston,                *
 *   MA 02111-1307, USA                                                    *
 ***************************************************************************/

#include    <algorithm>

#include    "help/aider.hpp"
#include    "help/string.hpp"
#include    "help/microtime.hpp"

#include    "antc/udpchannel.hpp"

using namespace std;
using namespace ant;
using namespace help;
using namespace zsocket;

namespace   antc
{

UdpChannel::UdpChannel(std::string server_address, int server_port)
    :   m_udper(), m_strServerAddress(server_address), m_iServerPort(server_port), 
        m_addressServer(m_strServerAddress, m_iServerPort),
        m_pFileTrace(NULL)
{
    if ( (server_address.size() < 1) || ('/' == server_address[0]) ) {
        throw ant::Exception(String::format("Invalid parameter : server_address='%s'", server_address.c_str()), ANTC_EPARAMETER);
    }
    if ( server_port < 1024 ) {
        throw ant::Exception(String::format("Invalid parameter : server_port=%d", server_port), ANTC_EPARAMETER);
    }

    m_udper.open();
    
    this->init();
}

UdpChannel::UdpChannel(zsocket::Udper& udper)
    :   m_udper(udper),
        m_pFileTrace(NULL)
{
    this->init();
}

UdpChannel::~UdpChannel()
{
    m_udper.close();
}


////////////////////////////////////////////////////////////////////////

void
UdpChannel::init()
{
    const char* pszFileTrace = getenv("ANTC_TRACE");
    if (NULL != pszFileTrace) {
        const char* pszTracePath = Aider::dirname(pszFileTrace);
        if (NULL != pszTracePath) {
            int iReturn = Aider::makedir(pszTracePath);
            if ( iReturn < 0 ) {
                fprintf(stderr, "Fail to mkdir '%s'.", pszTracePath);
            }
        }
        m_pFileTrace = fopen(pszFileTrace, "a");
        if ( NULL == m_pFileTrace ) {
            fprintf(stderr, "Fail to open ANTC_TRACE file '%s'.", pszFileTrace );
        }
        else {
            setbuf(m_pFileTrace, NULL);
        }
    }
}

void
UdpChannel::trace(const char* pszFormat, ...)
{
    if (NULL == m_pFileTrace) {
        return;
    }
    
    fprintf(m_pFileTrace, "%s ", Aider::getTimeString());
    va_list args;
    va_start(args, pszFormat);
    vfprintf(m_pFileTrace, pszFormat, args);
    va_end(args);
    fprintf(m_pFileTrace, "\n");
}

bool
UdpChannel::traceable(void)
{
    return  (NULL != m_pFileTrace);
}
    
////////////////////////////////////////////////////////////////////////

void
UdpChannel::send(Packet& packet, int iTimeout) throw (ant::Exception)
{
    this->send(m_addressServer, packet, iTimeout);
}

void
UdpChannel::recv(Packet& packet, int iTimeout) throw (ant::Exception)
{
    Address address;
    
    this->recv(address, packet, iTimeout);
}

////////////////////////////////////////////////////////////////////////

void
UdpChannel::send(zsocket::Address& address, Packet& packet, int iTimeout) throw (ant::Exception)
{
    if (!packet.isDataPacket()) {
        throw ant::Exception(String::format("Only DATA packet allowed (type=0x%04X).", packet.getType()), ANTC_EUNKNOWN);
    }

    unsigned    iSerialToCheck = packet.getSerial();
    bool    bGotSynPacket = false;
    bool    bGotAckPacket = false;
    
    bool    bTestTimeout = false;
    microtime_t    deadline;
    if (iTimeout >= 0) {
        deadline = microtime_t::microtime() + iTimeout;
        bTestTimeout = true;
    }

    while ( true ) {
        // STEP. check to see if timeout
        if ( bTestTimeout ) {
            microtime_t    now = microtime_t::microtime();
            if (now > deadline) {
                throw ant::Exception(String::format("%d seconds timeout.", iTimeout), ANTC_ETIMEOUT);
            }
        }
        
        int     iWaitTimeout = 1;
        
        // STEP. send DATA packet    
        if (false == bGotSynPacket) {
            try {
                if (this->traceable()) {
                    this->trace("SEND DATA packet(%s) to address(%s:%d)", packet.toString().c_str(), address.getIPString().c_str(), address.getPort());
                }
    
                m_udper.sendto(address, packet.getPack(), packet.getPackSize(), iWaitTimeout);
            }
            catch (zsocket::Exception& ex) {
                if (zsocket::Socket::TIMEOUT_CODE == ex.getCode()) {
                    continue;
                }

                throw ant::Exception(String::format("while send DATA packet, catch zsocket::Exception : [%d] %s", ex.getCode(), ex.what()), ANTC_EUNKNOWN);
            }
            catch (std::exception& ex) {
                throw ant::Exception(String::format("while send DATA packet, catch std::exception : %s", ex.what()), ANTC_EUNKNOWN);
            }
        }
        
        // STEP. wait incoming packet
        zsocket::Address addressIncome;
        std::string     strPacketIncome;
        try {
            m_udper.recvfrom(addressIncome, strPacketIncome, iWaitTimeout);
        }
        catch(zsocket::Exception& ex) {
            if (zsocket::Socket::TIMEOUT_CODE == ex.getCode()) {
                continue;
            }
            
            throw ex;
        }
        
        // STEP. handle incoming packet
        Packet  packetIncome(strPacketIncome);
        if (this->traceable()) {
            this->trace("RECV packet(%s) to address(%s:%d)", packetIncome.toString().c_str(), addressIncome.getIPString().c_str(), addressIncome.getPort());
        }
        if (packetIncome.isSynPacket()) {
            if (iSerialToCheck == packetIncome.getSerialFor()) {
                bGotSynPacket = true;
            }
            else {
                // discard
            }
        }
        else {
            // send SYN packet
            Packet  packetSYN(Packet::HEAD_SIZE);
            //packetSYN.setSerial();
            packetSYN.setSynPacketType();
            packetSYN.setSerialFor(packetIncome.getSerial());
            try {
                if (this->traceable()) {
                    this->trace("SEND SYN packet(%s) to address(%s:%d)", packetSYN.toString().c_str(), addressIncome.getIPString().c_str(), addressIncome.getPort());
                }
                m_udper.sendto(addressIncome, packetSYN.getPack(), packetSYN.getPackSize(), iWaitTimeout);
            }
            catch (zsocket::Exception& ex) {
                throw ant::Exception(String::format("while send SYN packet, catch zsocket::Exception : [%d] %s", ex.getCode(), ex.what()), ANTC_EUNKNOWN);
            }
            catch (std::exception& ex) {
                throw ant::Exception(String::format("while send SYN packet, catch std::exception : %s", ex.what()), ANTC_EUNKNOWN);
            }
            
            if (packetIncome.isAckPacket()) {
                if (iSerialToCheck == packetIncome.getSerialFor()) {
                    // finished send
                    bGotSynPacket = true;
                    bGotAckPacket = true;
                    break;      // break while(true)
                }else {
                    // unkown ACK packet
                    // if ACK for some DATA packet in queue ????
                    // or, discard it
                }
            }
            else if (packetIncome.isDataPacket()) {
                // put packet into DATA queue
                queue_serial_type::iterator result = find(m_serials_income.begin(), m_serials_income.end(), packetIncome.getSerial());
                if ( m_serials_income.end() != result ) {
                    Packet* _pPacket = new Packet(strPacketIncome);
                    zsocket::Address*   _pAddress = new zsocket::Address(addressIncome);
                    
                    PacketIncomeCell    cell(_pPacket->getSerial(), _pPacket, _pAddress);
                    m_queue_data_received.push_back(cell);
                
                    m_serials_income.push_front( _pPacket->getSerial() );
                    
                    if (m_serials_income.size() >= MAX_QUEUE_SERIALS_INCOME_LENGTH) {
                        m_serials_income.erase(m_serials_income.begin() + MIN_QUEUE_SERIALS_INCOME_LENGTH, m_serials_income.end());
                    }
                }
            }
            else {
                // unknown type packet
                // discard it
            }
        }
        
    }   // while(true)
        
    // STEP. 
}

void
UdpChannel::recv(zsocket::Address& address, Packet& packet, int iTimeout) throw (ant::Exception)
{
    if ( m_queue_data_received.size() > 0 ) {
        PacketIncomeCell&   cell = m_queue_data_received.front();
        m_queue_data_received.pop_front();
        
        address = *(cell.m_pAddress);
        packet = *(cell.m_pPacketData);
        
        return;
    }
    
    bool    bTestTimeout = false;
    microtime_t    deadline;
    if (iTimeout >= 0) {
        deadline = microtime_t::microtime() + iTimeout;
        bTestTimeout = true;
    }

    while ( true ) {
        // STEP. check to see if timeout
        if ( bTestTimeout ) {
            microtime_t    now = microtime_t::microtime();
            if (now > deadline) {
                throw ant::Exception(String::format("%d seconds timeout.", iTimeout), ANTC_ETIMEOUT);
            }
        }

        // STEP. receive packet
        int     iWaitTimeout = 1;
        zsocket::Address    addressIncome;
        string  strPacketIncome;
        
        try {
            m_udper.recvfrom(addressIncome, strPacketIncome, iWaitTimeout);
        }
        catch (zsocket::Exception& ex) {
            if (zsocket::Socket::TIMEOUT_CODE == ex.getCode()) {
                continue;
            }
            else {
                throw ant::Exception(String::format("zsocket::Exception : [%d] %s", ex.getCode(), ex.what()), ANTC_EUNKNOWN);
            }
        }
        catch (std::exception& ex) {
            throw ant::Exception(String::format("std::exception : %s", ex.what()), ANTC_EUNKNOWN);
        }
        
        // STEP. handle incoming packet
        Packet  packetIncome(strPacketIncome);
        if (this->traceable()) {
            this->trace("RECV packet(%s) to address(%s:%d)", packetIncome.toString().c_str(), addressIncome.getIPString().c_str(), addressIncome.getPort());
        }
        if (packetIncome.isSynPacket()) {
            // discard it
        }
        else {
            // send SYN packet
            Packet  packetSYN(Packet::HEAD_SIZE);
            //packetSYN.setSerial();
            packetSYN.setSynPacketType();
            packetSYN.setSerialFor(packetIncome.getSerial());
            try {
                if (this->traceable()) {
                    this->trace("SEND SYN packet(%s) to address(%s:%d)", packetSYN.toString().c_str(), addressIncome.getIPString().c_str(), addressIncome.getPort());
                }
                m_udper.sendto(addressIncome, packetSYN.getPack(), packetSYN.getPackSize(), iWaitTimeout);
            }
            catch (zsocket::Exception& ex) {
                throw ant::Exception(String::format("while send SYN packet, catch zsocket::Exception : [%d] %s", ex.getCode(), ex.what()), ANTC_EUNKNOWN);
            }
            catch (std::exception& ex) {
                throw ant::Exception(String::format("while send SYN packet, catch std::exception : %s", ex.what()), ANTC_EUNKNOWN);
            }
            
            if (packetIncome.isAckPacket()) {
                    // unkown ACK packet
                    // if ACK for some DATA packet in queue ????
                    // or, discard it
            }
            else if (packetIncome.isDataPacket()) {
                // put packet into DATA queue
                address = addressIncome;
                packet = packetIncome;
                
                break;      // break while(true)
            }
            else {
                // unknown type packet
                // discard it
            }
        }
    
    }   // while (true)
}

}   // namespace antc
